<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue拖拽滚动指令</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
        color: #333;
        line-height: 1.6;
        padding: 20px;
        min-height: 100vh;
      }

      .container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }

      header {
        text-align: center;
        margin-bottom: 40px;
      }

      h1 {
        color: #2c3e50;
        margin-bottom: 10px;
        font-size: 2.5rem;
      }

      .subtitle {
        color: #7f8c8d;
        font-size: 1.2rem;
        max-width: 600px;
        margin: 0 auto;
      }

      .demo-section {
        display: flex;
        flex-wrap: wrap;
        gap: 30px;
        margin-bottom: 40px;
      }

      .demo-card {
        flex: 1;
        min-width: 300px;
        background: white;
        border-radius: 12px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
        padding: 25px;
      }

      .demo-title {
        color: #2c3e50;
        margin-bottom: 20px;
        padding-bottom: 10px;
        border-bottom: 2px solid #ecf0f1;
        font-size: 1.5rem;
      }

      .scroll-container {
        width: 100%;
        height: 300px;
        overflow: hidden;
        position: relative;
        border-radius: 8px;
        background: #f8f9fa;
        box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
      }

      .scroll-content {
        padding: 20px;
        min-height: 100%;
      }

      .item {
        padding: 15px;
        margin-bottom: 10px;
        background: white;
        border-radius: 8px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
        border-left: 4px solid #3498db;
      }

      .item:nth-child(2n) {
        border-left-color: #2ecc71;
      }

      .item:nth-child(3n) {
        border-left-color: #e74c3c;
      }

      .horizontal-scroll {
        display: flex;
        width: max-content;
        padding: 15px;
      }

      .horizontal-item {
        flex: 0 0 200px;
        height: 120px;
        margin-right: 15px;
        background: white;
        border-radius: 8px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: bold;
        color: white;
        box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
      }

      .horizontal-item:nth-child(1) {
        background: #3498db;
      }
      .horizontal-item:nth-child(2) {
        background: #2ecc71;
      }
      .horizontal-item:nth-child(3) {
        background: #e74c3c;
      }
      .horizontal-item:nth-child(4) {
        background: #f39c12;
      }
      .horizontal-item:nth-child(5) {
        background: #9b59b6;
      }
      .horizontal-item:nth-child(6) {
        background: #1abc9c;
      }
      .horizontal-item:nth-child(7) {
        background: #34495e;
      }
      .horizontal-item:nth-child(8) {
        background: #e67e22;
      }
      .horizontal-item:nth-child(9) {
        background: #27ae60;
      }

      .instructions {
        background: white;
        border-radius: 12px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
        padding: 30px;
        margin-top: 40px;
      }

      .instructions h2 {
        color: #2c3e50;
        margin-bottom: 20px;
        padding-bottom: 10px;
        border-bottom: 2px solid #ecf0f1;
      }

      .code-block {
        background: #2c3e50;
        color: #ecf0f1;
        padding: 20px;
        border-radius: 8px;
        margin: 20px 0;
        overflow-x: auto;
        font-family: 'Courier New', monospace;
        font-size: 0.9rem;
      }

      .note {
        background: #e8f4fc;
        border-left: 4px solid #3498db;
        padding: 15px;
        margin: 20px 0;
        border-radius: 0 8px 8px 0;
      }

      .status {
        text-align: center;
        margin-top: 15px;
        color: #7f8c8d;
        font-style: italic;
      }

      footer {
        text-align: center;
        margin-top: 40px;
        color: #7f8c8d;
        font-size: 0.9rem;
      }

      @media (max-width: 768px) {
        .demo-section {
          flex-direction: column;
        }
      }
    </style>
  </head>
  <body>
    <div id="app" class="container">
      <header>
        <h1>Vue拖拽滚动指令</h1>
        <p class="subtitle">模拟移动端设备的拖拽滚动效果，支持触摸和鼠标操作</p>
      </header>

      <div class="demo-section">
        <div class="demo-card">
          <h2 class="demo-title">垂直滚动示例</h2>
          <div class="scroll-container" v-drag-scroll>
            <div class="scroll-content">
              <div class="item" v-for="i in 20" :key="i">
                项目 {{ i }} - 拖拽我进行滚动
              </div>
            </div>
          </div>
          <div class="status">滚动状态: {{ status }}</div>
        </div>

        <div class="demo-card">
          <h2 class="demo-title">水平滚动示例</h2>
          <div class="scroll-container" v-drag-scroll>
            <div class="horizontal-scroll">
              <div class="horizontal-item" v-for="i in 9" :key="i">
                项目 {{ i }}
              </div>
            </div>
          </div>
          <div class="status">滚动状态: {{ status }}</div>
        </div>
      </div>

      <div class="instructions">
        <h2>实现原理</h2>
        <p>这个Vue自定义指令通过以下步骤实现拖拽滚动：</p>

        <div class="code-block">
          // Vue自定义指令 Vue.directive('drag-scroll', { bind(el, binding) {
          let isDragging = false; let startX, startY; let scrollLeft, scrollTop;
          let velocityX = 0, velocityY = 0; let lastX, lastY; let timestamp; let
          rafId; // 触摸开始事件 const touchStart = (e) => { isDragging = true;
          const touch = e.touches ? e.touches[0] : e; startX = touch.clientX;
          startY = touch.clientY; scrollLeft = el.scrollLeft; scrollTop =
          el.scrollTop; velocityX = 0; velocityY = 0; lastX = startX; lastY =
          startY; timestamp = Date.now(); el.style.cursor = 'grabbing';
          el.style.userSelect = 'none'; }; // 触摸移动事件 const touchMove = (e)
          => { if (!isDragging) return; e.preventDefault(); const touch =
          e.touches ? e.touches[0] : e; const x = touch.clientX; const y =
          touch.clientY; const walkX = (x - startX) * 2; // 乘以2增加灵敏度
          const walkY = (y - startY) * 2; el.scrollLeft = scrollLeft - walkX;
          el.scrollTop = scrollTop - walkY; // 计算速度 const now = Date.now();
          const elapsed = now - timestamp; if (elapsed > 0) { velocityX = (x -
          lastX) / elapsed; velocityY = (y - lastY) / elapsed; } lastX = x;
          lastY = y; timestamp = now; }; // 触摸结束事件 const touchEnd = () =>
          { isDragging = false; el.style.cursor = 'grab'; el.style.userSelect =
          ''; // 惯性滚动 if (Math.abs(velocityX) > 0.1 || Math.abs(velocityY) >
          0.1) { inertialScroll(); } }; // 惯性滚动 const inertialScroll = () =>
          { const decay = 0.95; // 衰减系数 velocityX *= decay; velocityY *=
          decay; el.scrollLeft -= velocityX * 10; el.scrollTop -= velocityY *
          10; if (Math.abs(velocityX) > 0.01 || Math.abs(velocityY) > 0.01) {
          rafId = requestAnimationFrame(inertialScroll); } }; // 添加事件监听器
          el.addEventListener('mousedown', touchStart);
          el.addEventListener('mousemove', touchMove);
          el.addEventListener('mouseup', touchEnd);
          el.addEventListener('mouseleave', touchEnd);
          el.addEventListener('touchstart', touchStart);
          el.addEventListener('touchmove', touchMove, { passive: false });
          el.addEventListener('touchend', touchEnd); // 设置初始光标样式
          el.style.cursor = 'grab'; // 清理函数 el.cleanup = () => {
          cancelAnimationFrame(rafId); el.removeEventListener('mousedown',
          touchStart); el.removeEventListener('mousemove', touchMove);
          el.removeEventListener('mouseup', touchEnd);
          el.removeEventListener('mouseleave', touchEnd);
          el.removeEventListener('touchstart', touchStart);
          el.removeEventListener('touchmove', touchMove);
          el.removeEventListener('touchend', touchEnd); }; }, unbind(el) { if
          (el.cleanup) { el.cleanup(); } } });
        </div>

        <div class="note">
          <p><strong>特性说明：</strong></p>
          <ul>
            <li>支持触摸和鼠标操作</li>
            <li>实现惯性滚动效果</li>
            <li>自动清理事件监听器</li>
            <li>提供视觉反馈（光标变化）</li>
          </ul>
        </div>

        <p>
          使用此指令非常简单，只需在需要拖拽滚动的元素上添加
          <code>v-drag-scroll</code> 指令即可。
        </p>
      </div>

      <footer>
        <p>Vue拖拽滚动指令示例 &copy; 2023</p>
      </footer>
    </div>

    <script>
      // Vue自定义指令
      Vue.directive('drag-scroll', {
        bind(el, binding) {
          let isDragging = false
          let startX, startY
          let scrollLeft, scrollTop
          let velocityX = 0,
            velocityY = 0
          let lastX, lastY
          let timestamp
          let rafId

          // 触摸开始事件
          const touchStart = (e) => {
            isDragging = true
            const touch = e.touches ? e.touches[0] : e
            startX = touch.clientX
            startY = touch.clientY
            scrollLeft = el.scrollLeft
            scrollTop = el.scrollTop
            velocityX = 0
            velocityY = 0
            lastX = startX
            lastY = startY
            timestamp = Date.now()

            el.style.cursor = 'grabbing'
            el.style.userSelect = 'none'

            // 更新状态
            if (binding.instance) {
              binding.instance.status = '拖拽中...'
            }
          }

          // 触摸移动事件
          const touchMove = (e) => {
            if (!isDragging) return
            e.preventDefault()

            const touch = e.touches ? e.touches[0] : e
            const x = touch.clientX
            const y = touch.clientY

            const walkX = (x - startX) * 2 // 乘以2增加灵敏度
            const walkY = (y - startY) * 2

            el.scrollLeft = scrollLeft - walkX
            el.scrollTop = scrollTop - walkY

            // 计算速度
            const now = Date.now()
            const elapsed = now - timestamp

            if (elapsed > 0) {
              velocityX = (x - lastX) / elapsed
              velocityY = (y - lastY) / elapsed
            }

            lastX = x
            lastY = y
            timestamp = now
          }

          // 触摸结束事件
          const touchEnd = () => {
            isDragging = false
            el.style.cursor = 'grab'
            el.style.userSelect = ''

            // 更新状态
            if (binding.instance) {
              binding.instance.status = '惯性滚动中...'
            }

            // 惯性滚动
            if (Math.abs(velocityX) > 0.1 || Math.abs(velocityY) > 0.1) {
              inertialScroll()
            } else {
              if (binding.instance) {
                binding.instance.status = '就绪'
              }
            }
          }

          // 惯性滚动
          const inertialScroll = () => {
            const decay = 0.95 // 衰减系数

            velocityX *= decay
            velocityY *= decay

            el.scrollLeft -= velocityX * 10
            el.scrollTop -= velocityY * 10

            if (Math.abs(velocityX) > 0.01 || Math.abs(velocityY) > 0.01) {
              rafId = requestAnimationFrame(inertialScroll)
            } else {
              if (binding.instance) {
                binding.instance.status = '就绪'
              }
            }
          }

          // 添加事件监听器
          el.addEventListener('mousedown', touchStart)
          el.addEventListener('mousemove', touchMove)
          el.addEventListener('mouseup', touchEnd)
          el.addEventListener('mouseleave', touchEnd)

          el.addEventListener('touchstart', touchStart)
          el.addEventListener('touchmove', touchMove, { passive: false })
          el.addEventListener('touchend', touchEnd)

          // 设置初始光标样式
          el.style.cursor = 'grab'

          // 清理函数
          el.cleanup = () => {
            cancelAnimationFrame(rafId)
            el.removeEventListener('mousedown', touchStart)
            el.removeEventListener('mousemove', touchMove)
            el.removeEventListener('mouseup', touchEnd)
            el.removeEventListener('mouseleave', touchEnd)

            el.removeEventListener('touchstart', touchStart)
            el.removeEventListener('touchmove', touchMove)
            el.removeEventListener('touchend', touchEnd)
          }
        },

        unbind(el) {
          if (el.cleanup) {
            el.cleanup()
          }
        },
      })

      // Vue实例
      new Vue({
        el: '#app',
        data() {
          return {
            status: '就绪',
          }
        },
      })
    </script>
  </body>
</html>
